{{!

  Copyright 2016 Facebook, Inc.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

}}{{!

Generates a top-level file to be imported in the user's client code.

The generated file is pretty big, but the bulk of the generation is done inside
the clients/Callbacks partial.

}}
{{> common/AutoGeneratedPy}}
from libcpp.memory cimport shared_ptr, make_shared, unique_ptr, make_unique
from libcpp.string cimport string
from libcpp cimport bool as cbool
from cpython cimport bool as pbool
from libc.stdint cimport int8_t, int16_t, int32_t, int64_t
from libcpp.vector cimport vector as vector
from libcpp.set cimport set as cset
from libcpp.map cimport map as cmap
from cython.operator cimport dereference as deref, typeid
from cpython.ref cimport PyObject
from thrift.py3.client cimport cRequestChannel_ptr, makeClientWrapper
from thrift.py3.exceptions cimport try_make_shared_exception, create_py_exception
from folly cimport cFollyTry, cFollyUnit, c_unit
from libcpp.typeinfo cimport type_info
import thrift.py3.types
cimport thrift.py3.types
import thrift.py3.client
cimport thrift.py3.client
from thrift.py3.common cimport RpcOptions as __RpcOptions
from thrift.py3.common import RpcOptions as __RpcOptions

from folly.futures cimport bridgeFutureWith
from folly.executor cimport get_executor
cimport cython

import sys
import types as _py_types
from asyncio import get_event_loop as asyncio_get_event_loop, shield as asyncio_shield, InvalidStateError as asyncio_InvalidStateError

cimport {{#program:py3Namespaces}}{{value}}.{{/program:py3Namespaces}}{{program:name}}.types as _{{#program:py3Namespaces}}{{value}}_{{/program:py3Namespaces}}{{program:name}}_types
import {{#program:py3Namespaces}}{{value}}.{{/program:py3Namespaces}}{{program:name}}.types as _{{#program:py3Namespaces}}{{value}}_{{/program:py3Namespaces}}{{program:name}}_types
{{#program:includeNamespaces}}
{{#hasTypes?}}
cimport {{#includeNamespace}}{{value}}.{{/includeNamespace}}types as _{{#includeNamespace}}{{value}}_{{/includeNamespace}}types
import {{#includeNamespace}}{{value}}.{{/includeNamespace}}types as _{{#includeNamespace}}{{value}}_{{/includeNamespace}}types
{{/hasTypes?}}
{{#hasServices?}}
cimport {{#includeNamespace}}{{value}}.{{/includeNamespace}}clients as _{{#includeNamespace}}{{value}}_{{/includeNamespace}}clients
import {{#includeNamespace}}{{value}}.{{/includeNamespace}}clients as _{{#includeNamespace}}{{value}}_{{/includeNamespace}}clients
{{/hasServices?}}
{{/program:includeNamespaces}}

{{#program:services}}
from {{#program:py3Namespaces}}{{value}}.{{/program:py3Namespaces}}{{program:name}}.clients_wrapper cimport c{{service:name}}AsyncClient, c{{service:name}}ClientWrapper
{{#service:extends}}
{{#service:externalProgram?}}
from {{#service:py3Namespaces}}{{value}}.{{/service:py3Namespaces}}{{service:programName}}.clients_wrapper cimport c{{service:name}}ClientWrapper
{{/service:externalProgram?}}
{{/service:extends}}
{{/program:services}}

{{> clients/Callbacks}}

{{#program:services}}
cdef object _{{service:name}}_annotations = _py_types.MappingProxyType({
{{#service:annotations}}
    """{{annotation:key}}""": {{!
        }}{{#annotation:value?}}"{{annotation:value}}"{{/annotation:value?}}{{!
        An annotation without an explicitly provided value is implicitly assigned the default value "1".
        }}{{^annotation:value?}}"1"{{/annotation:value?}},
{{/service:annotations}}
})


cdef class {{service:name}}({{#service:extends}}{{#service:externalProgram?}}{{!
    }}_{{#service:py3Namespaces}}{{value}}_{{/service:py3Namespaces}}{{!
    }}{{service:programName}}_clients.{{/service:externalProgram?}}{{service:name}}{{!
  }}{{/service:extends}}{{^service:extends?}}thrift.py3.client.Client{{/service:extends?}}):
    annotations = _{{service:name}}_annotations

    def __cinit__({{service:name}} self):
        loop = asyncio_get_event_loop()
        self._connect_future = loop.create_future()
        self._deferred_headers = {}

    cdef const type_info* _typeid({{service:name}} self):
        return &typeid(c{{service:name}}AsyncClient)

    @staticmethod
    cdef _{{program:name}}_{{service:name}}_set_client({{service:name}} inst, shared_ptr[c{{service:name}}ClientWrapper] c_obj):
        """So the class hierarchy talks to the correct pointer type"""
        inst._{{program:name}}_{{service:name}}_client = c_obj
{{#service:extends}}
        {{#service:externalProgram?}}_{{#service:py3Namespaces}}{{value}}_{{/service:py3Namespaces}}{{service:programName}}_clients.{{/service:externalProgram?}}{{service:name}}._{{service:programName}}_{{service:name}}_set_client(inst, <shared_ptr[c{{service:name}}ClientWrapper]>c_obj)
{{/service:extends}}

    cdef _{{program:name}}_{{service:name}}_reset_client({{service:name}} self):
        """So the class hierarchy resets the shared pointer up the chain"""
        self._{{program:name}}_{{service:name}}_client.reset()
{{#service:extends}}
        {{#service:externalProgram?}}_{{#service:py3Namespaces}}{{value}}_{{/service:py3Namespaces}}{{service:programName}}_clients.{{/service:externalProgram?}}{{service:name}}._{{service:programName}}_{{service:name}}_reset_client(self)
{{/service:extends}}

    def __dealloc__({{service:name}} self):
        if self._connect_future.done() and not self._connect_future.exception():
            print(f'thrift-py3 client: {self!r} was not cleaned up, use the async context manager', file=sys.stderr)
            if self._{{program:name}}_{{service:name}}_client:
                deref(self._{{program:name}}_{{service:name}}_client).disconnect().get()
        self._{{program:name}}_{{service:name}}_reset_client()

    cdef bind_client({{service:name}} self, cRequestChannel_ptr&& channel):
        {{service:name}}._{{program:name}}_{{service:name}}_set_client(
            self,
            makeClientWrapper[c{{service:name}}AsyncClient, c{{service:name}}ClientWrapper](
                thrift.py3.client.move(channel)
            ),
        )

    async def __aenter__({{service:name}} self):
        await asyncio_shield(self._connect_future)
        if self._context_entered:
            raise asyncio_InvalidStateError('Client context has been used already')
        self._context_entered = True
        {{!Replay setting persistent_headers after the C++ client is ready}}
        for key, value in self._deferred_headers.items():
            self.set_persistent_header(key, value)
        self._deferred_headers = None
        return self

    def __aexit__({{service:name}} self, *exc):
        self._check_connect_future()
        loop = asyncio_get_event_loop()
        future = loop.create_future()
        userdata = (self, future)
        bridgeFutureWith[cFollyUnit](
            self._executor,
            deref(self._{{program:name}}_{{service:name}}_client).disconnect(),
            closed_{{service:name}}_py3_client_callback,
            <PyObject *>userdata  # So we keep client alive until disconnect
        )
        # To break any future usage of this client
        # Also to prevent dealloc from trying to disconnect in a blocking way.
        badfuture = loop.create_future()
        badfuture.set_exception(asyncio_InvalidStateError('Client Out of Context'))
        {{! We do this to make sure this exception was recieved by someone}}
        badfuture.exception()
        {{! All calls to the client will now raise}}
        self._connect_future = badfuture
        return asyncio_shield(future)

    def set_persistent_header({{service:name}} self, str key, str value):
        {{! If we are called before aenter then save the header for later}}
        if not self._{{program:name}}_{{service:name}}_client:
            self._deferred_headers[key] = value
            return

        cdef string ckey = <bytes> key.encode('utf-8')
        cdef string cvalue = <bytes> value.encode('utf-8')
        deref(self._{{program:name}}_{{service:name}}_client).setPersistentHeader(ckey, cvalue)

    {{#service:functions}}
    @cython.always_allow_keywords(True)
    def {{function:name}}(
            {{service:name}} self{{#function:args}},
            {{#field:type}}{{!
                }}{{#type:hasCythonType?}}{{^type:integer?}}{{> types/CythonPythonType}} {{/type:integer?}}{{/type:hasCythonType?}}{{!
                }}{{field:name}}{{#type:integer?}} not None{{/type:integer?}}{{^type:number?}} not None{{/type:number?}}{{!
            }}{{/field:type}}{{/function:args}},
            __RpcOptions rpc_options=None
    ):
        if rpc_options is None:
            rpc_options = <__RpcOptions>__RpcOptions.__new__(__RpcOptions)
        {{#function:args}}
        {{#field:type}}
        {{#type:container?}}
        if not isinstance({{field:name}}, {{> types/PythonType}}):
            {{field:name}} = {{> types/PythonType}}({{field:name}})
        {{/type:container?}}
        {{#type:integer?}}
        if not isinstance({{field:name}}, int):
            raise TypeError(f'{{field:name}} is not a {int !r}.')
        else:
            {{! inject cython int Overflow checks }}
            {{field:name}} = <{{> types/CythonPythonType}}> {{field:name}}
        {{/type:integer?}}
        {{/field:type}}
        {{/function:args}}
        self._check_connect_future()
        __loop = asyncio_get_event_loop()
        __future = __loop.create_future()
        __userdata = (self, __future, rpc_options)
        bridgeFutureWith[{{#function:returnType}}{{> types/CythonCppType}}{{/function:returnType}}](
            self._executor,
            deref(self._{{program:name}}_{{service:name}}_client).{{function:name}}(rpc_options._cpp_obj, {{#function:args}}
                {{#field:type}}{{> CythonPythonToCppArg}}{{/field:type}},{{/function:args}}
            ),
            {{service:name}}_{{function:name}}_callback,
            <PyObject *> __userdata
        )
        return asyncio_shield(__future)

    {{/service:functions}}


cdef void closed_{{service:name}}_py3_client_callback(
    cFollyTry[cFollyUnit]&& result,
    PyObject* userdata,
):
    client, pyfuture = <object> userdata {{! we keep the client alive until we exit }}
    pyfuture.set_result(None)
{{/program:services}}
